package ru.evga.inse.core.dao.impl;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;

import org.springframework.transaction.annotation.Transactional;

import ru.evga.inse.core.dao.DaoException;
import ru.evga.inse.core.dao.FindRangeContext;
import ru.evga.inse.core.dao.IDao;
import ru.evga.inse.core.dao.IResultContainer;
import ru.evga.inse.core.dao.Parameter;
import ru.evga.inse.core.domain.BaseEntity;


public class GenericDao<E extends BaseEntity>
    implements IDao<Long, E>
{
    private Class<E> m_entityClass;
    @PersistenceContext
    protected EntityManager m_entityManager;
    private Object lockObject = new Object();
    private CriteriaBuilder builder;
    
    private Set<String> entutyFieldSet;

    @SuppressWarnings("unchecked")
    public GenericDao()
    {
        ParameterizedType genericSuperclass = (ParameterizedType)getClass().getGenericSuperclass();
        m_entityClass = (Class<E>)genericSuperclass.getActualTypeArguments()[0];
    }

    public Class<E> getEntityClass()
    {
        return m_entityClass;
    }

    public String getEntityName()
    {
        return m_entityClass.getSimpleName();
    }
    
    @Transactional(readOnly = true)
    public E find(Long id) throws DaoException
    {
        checkId(id);
        E result = m_entityManager.find(m_entityClass, id);
        if(result == null)
        {
            throw new DaoException(DaoException.IDS_ENTITY_WITH_ID_NOT_FOUND, new Object[]{getEntityName(), id});
        }
        return result;
    }

    public E getReference(Long id) throws DaoException
    {
        checkId(id);
        return m_entityManager.getReference(m_entityClass, id);
    }

    public E getReference(E entity) throws DaoException
    {
        if(entity == null)
        {
            throw new DaoException(DaoException.IDS_ENTITY_MUST_NOT_BE_NULL, m_entityClass.getSimpleName());
        }
        return getReference(entity.getId());
    }
    
    @Transactional
    public E persist(E entity)
    {
        m_entityManager.persist(entity);
        return entity;
    }

    @Transactional
    public void remove(E entity) throws DaoException
    {
        if (m_entityManager.contains(entity))
        {
            this.m_entityManager.remove(entity);
        }
        else
        {
            remove(entity.getId());
        }
    }

    @Transactional
    public void remove(Long id) throws DaoException
    {
        E attached = find(id);
        m_entityManager.remove(attached);
    }

    @Transactional
    public void flush()
    {
        m_entityManager.flush();
    }

    @Transactional
    public E merge(E entity)
    {
        E merged = m_entityManager.merge(entity);
        m_entityManager.flush();
        return merged;
    }

    @Transactional(readOnly=true)
    protected IResultContainer<E> findRange(
        final Integer firstResult, 
        final Integer maxResults, 
        FindRangeContext<E> queryBuilder) throws DaoException
    {
        int first = firstResult < 0 ? 0 : firstResult;
        int max = maxResults < 0 ? 0 : maxResults;
        
        max = max + 1; //fetch on one more for test on has next

        CriteriaQuery<E> criteriaQuery = queryBuilder.getCriteriaQuery();

        TypedQuery<E> query = m_entityManager.createQuery(criteriaQuery);

        for(Parameter p : queryBuilder.getParameters())
        {
            query.setParameter(p.name, p.value);
        }
        
        List<E> entityList = (List<E>)query.setFirstResult(first).setMaxResults(max).getResultList();
        
        ResultContainer<E> resultContainer = new ResultContainer<E>();
        resultContainer.setHasPrevious(first > 0 && !entityList.isEmpty());
        
        boolean hasNext = max == entityList.size();
        
        resultContainer.setHasNext(hasNext);
        
        if(hasNext)
        {
            entityList.remove(entityList.size() - 1);//remove last extra result
        }
        
        resultContainer.setDataContainer(entityList);
        
        return resultContainer;
    }

    
    
    private Object builderLocker = new Object();
    
    public CriteriaBuilder getCriteriaBuilder()
    {
        
        if(builder == null)
        {
            synchronized (builderLocker)
            {
                if(builder == null)
                {
                    builder = m_entityManager.getCriteriaBuilder(); 
                }
            }
        }
        return builder;
    }
    
    public CriteriaQuery<E> crateCriteriaQuery()
    {
        
        CriteriaQuery<E> criteria = getCriteriaBuilder().createQuery(getEntityClass());
        return criteria;
    }
    
    
    @Transactional(readOnly=true)
    public Long count()
    {
        return m_entityManager.createQuery("select count(e) from " + m_entityClass.getName() + " e ",
            Long.class).getSingleResult();
    }
    
    public void assertEntityHasField(String field) throws DaoException
    {
        if(entutyFieldSet == null)
        {
            synchronized (lockObject)
            {
                if(entutyFieldSet == null)
                {
                    entutyFieldSet = new HashSet<String>();
                    fillEntityField(entutyFieldSet, getEntityClass());
                }
            }
        }
        if(!entutyFieldSet.contains(field))
        {
            throw new DaoException(DaoException.IDS_ENTITY_DO_NOT_HAVE_FIELD, new Object[]{getEntityClass().getName(), field});
        }
    }
    
    public EntityManager getEntityManager()
    {
        return m_entityManager;
    }
    
    static private void fillEntityField(Set<String> fields, Class<?> clazz)
    {
        
        Class<?> superClass = clazz.getSuperclass();
        if(superClass != null)
        {
            fillEntityField(fields, superClass);//recurse
        }
        
        Field[] fieldArray = clazz.getDeclaredFields();
        for(Field f : fieldArray)
        {
            fields.add(f.getName());
        }
    }
    
    private void checkId(Long id) throws DaoException
    {
        if(id == null)
        {
            throw new DaoException(DaoException.IDS_ID_MUST_NOT_BE_NULL, m_entityClass.getSimpleName());
        }
        
    }
    
    
}
